跳到主要内容

Go 使用 goyacc 实现四则运算解析

GoYACC 是 Golang 版本的 YACC,本篇博文参考官网的 例子

其实语法解析 就是根据 token 去拼出多个解析规则,然后通过递归下降等算法去解析。 而 yacc 就是是通过 .y 描述文件 生成递归下降程序(y.go程序)实现语法解析的一个过程。

首先安装:

go get -u golang.org/x/tools/cmd/goyacc
参数说明
-l显示line指令
-o string指定输出解析器的文件名称 (默认 y.go)
-p string指定解析器输出接口的前缀
-v string生成解析过程表 (默认 y.output)

goyacc 会生成一个语义解析代码,其中它内部有两个重要的 interface, 其中 yyLexer 需要使用者自己实现提供,yacc 会生成 yyParser 的实现,其使用 yyLexer 做解释操作。解释的过程和解释前后都可以嵌入自己的代码逻辑,完成一个程序或者单纯生成一个自定义的语法树结构.

YACC 文件的写法

首先来看一下这个 yacc 文件(以 .y 后缀)的语法

{%
嵌入代码: go 代码
%}

文法定义: 由 %union %type %token %left %right %start 等组成的定义

%%
文法规则: 由 非终结符 与 终结符 组成的匹配 + 动作规则
%%

嵌入代码 (这部分为可选,比如可以 lexer 或者 main 可以写在这里或者单独用文件写 )
名称解释
嵌入代码golang代码 通常用来定义生成的文件的包名、引入包、结构定义等
文法定义由 %union %type %token %left %right %start 等组成的定义
文法规则由 非终结符 与终结符组成的规则

文法定义

简单说明如下:

描述符说明
%union用来定义一个类型并映射 golang 的一个数据类型
%struct同 %union 建议使用 %union
%token定义终结符,表示不能再拆了的字符 是一个 union 中定义的类型
%type定义非终结符
%start定义从哪个终结符开始解析 默认规则段中的第一个终结符
%left定义规则结合性质 左优先
%right定义规则结合性质 右优先
%nonasso定义规则结合性质 不结合
%perc term定义优先级与 term 一致

定义一个 val 类型 映射到 golang 的string类型, 定义一个stu 映射到 golang 中定义的一个 Student 结构体类型

%union {
val string
stu Student
}

文法规则

非终结符: 规则描述 {
动作描述
};

这个 “:” 左边的是非终结符 右侧的规则 通常由 非终结符 终结符 构成

非终结符:使用 %type 定义的符号 可以理解为一串特定排序的 token

终结符:可以理解成一个 token

规则描述:可以是 非终结符、终结符, 可用 | 分割多个规则,{} 内是具体动作,{} 内符合 Go 语法,并且 $$ 代表规约后的值, $1$2 代表类型变量值动作描述

如下:这个 $$ 表示 expr1,$1 表示第一个 NUM,$3 代表第二个 NUM

expr1: NUM '+' NUM {
$$ = $1 + $3
};

用 “{” 与 “}” 包裹的部分符合 Go 语法且拥有特殊宏替换的语法块,一条规则最后要用 “;” 结束

例子1:

expr1: NUM '+' NUM {
$$ = $1 + $3
};

这个例子定义了 一个 两数求和的规则 (a+b)

expr 构成终结符这个非终结符的条件有三个终结符(token)构成 NUM '+' NUM ,方法是求两数之和并返回给 expr1

例子2:

当需要定一个多个数求和时就需要通过递归定义了

expr2: expr1 '+' NUM {
$$ = $1 + $3
};

这个例子表示三个数求和,并使用了一个非终结符作为规则的一部分

例子3:

expr3: expr3 '+' NUM {
$$ = $1 + $3
} | NUM '+' NUM {
$$ = $1 + $3
};

这个例子通过递归定义了 支持多个数相加的规则 其中 使用 “|” 分割了两个规则描述

实现 lexer 接口

goyacc 没有对应的 lex 生成工具 通常需要手工写两个方法来实现 yyLexer 接口

type yyLexer interface {
Lex(lval *yySymType) int
Error(s string)
}

介绍这两个函数:

  • Lex() 词法分析函数 解析过程会多次回调此方法,每次调用应返一个token值,lval 是当前栈状态值
  • Error() 错误回调方法 在语法解析错误时被回调

一般可以通过 text/scanner 进行基础解析,再换算成自己的token,省去部分烦劳工作

简单的 Hello World

以下创建一个最简单的 HelloWorld 为例,文件名为 hello.y

%{
package main
import "fmt"
%}

%union {
type_string string
}

%type <type_string> expression

%token <type_string> PRINT HELLO

%%
expression: HELLO
{
fmt.Println("Hello World!")
}
%%

生成文件

goyacc -o hello.go -p hello hello.y

这句代码表示使用的 y 源文件为 hello.y,生成的目标 Parser 代码为 hello.go

上面说过,Lexer 可以写在另外一个文件里面,这里实习 helloLexer 接口

type helloLexer interface {
// 对输入的一行命令执行逐字符扫描,返回匹配到的 Token
Lex(lval *helloSymType) int
// 输入的文本无法在文件中找到对应的 Token 的时候应该做啥
Error(s string)
}

在 lexer.go 文件里面实现这个接口

package main

import (
"fmt"
"time"
)

type myLexer struct {
//待解析指令暂时存input里面,用byte数组方便逐字符操作
input []byte
//当前扫描到字符的索引
index int
//存储已经扫描到的内容,当内容为hello时,返回HELLO
buffer string
}

func (m *myLexer) Lex(lval *helloSymType) int {
if len(m.input) == 0 {
return 0 //表示已经解析完所有的内容了
}
for i := 0; i < len(m.input); i++ {
char := m.input[i]
switch char {
case 'h', 'e', 'l', 'o':
m.buffer += string(char)
}
if m.buffer == "hello" {
return HELLO
}
}
return 0
}

func (m *myLexer) Error(s string) {
fmt.Println(s)
}

func main() {
//正确使用
helloParse(&myLexer{
input: []byte("hello"),
index: 0,
buffer: "",
})
time.Sleep(time.Second)
//未定义语法,会报错
helloParse(&myLexer{
input: []byte("hhhhhh"),
index: 0,
buffer: "",
})
}

输出:

Hello World!
syntax error

自定义四则运算

词法分析模块:词法分析模块主要将输入的表达式转化为一个个的 token 转交语法分析模块处理。

比如表达式 “1” 这个被词法分析解析成一个 “NUMBER” 类型,并将该类型的值 1 存储在 l.val.num 中,这个变量定义在 语法分析模块中的 %union 域中。

%union {
num float64
}

%type <num> expression term factor
%token '+' '-' '*' '/' '(' ')'
%token <num> NUMBER

语法分析模块:这个模块本质就是按照 yacc 的语法定义文法,goyacc 会将该文件转化为一个分析引擎。

可以在 VsCode 下载一个语法高亮插件

完整的 calcyacc.y 文件

%{
package main
%}

%union {
num float64
}

%type <num> expression term factor
%token '+' '-' '*' '/' '(' ')'
%token <num> NUMBER

%%
top : expression
{
if l, ok := yylex.(*simpleLex); ok {
l.value = $1
}
}
;
expression : expression '+' term
{ $$ = $1 + $3 }
| expression '-' term
{ $$ = $1 - $3 }
| term
{ $$ = $1 }
;
term : term '*' factor
{ $$ = $1 * $3 }
| term '/' factor
{ $$ = $1 / $3 }
| factor
{ $$ = $1 }
;
factor : NUMBER
{ $$ = $1 }
| '(' expression ')'
{ $$ = $2 }
;
%%

然后实现生成的接口:

type yyLexer interface {
Lex(lval *yySymType) int
Error(s string)
}
package main

import (
"log"
"strconv"
"strings"
"text/scanner"
)

var LexPrintToken = false

type Token struct {
Type int
Str string
}

type simpleLex struct {
scanner.Scanner
value float64
}

// 这个接口必须实现,是词法分析的入口
func (s *simpleLex) Lex(lval *yySymType) int {
r, lit := s.Scan(), s.TokenText()
var token Token
token.Str = lit

switch r {
case scanner.EOF:
return 0
case scanner.Int:
i, _ := strconv.Atoi(lit)
lval.num = float64(i)
token.Type = scanner.Float
case scanner.Float:
lval.num, _ = strconv.ParseFloat(lit, 64)
token.Type = scanner.Float
default:
token.Type = int(r)
}

if token.Type == scanner.Float {
return NUMBER
} else {
return token.Type
}
}

// 词法分析异常处理 该接口必须实现
func (s *simpleLex) Error(s1 string) {
log.Printf("parse error: %s", s1)
}

// 计算入口
func Parse(code string) float64 {
s := new(simpleLex)
s.Init(strings.NewReader(code))
yyParse(s)
return s.value
}

编写单元测试

package main

import (
"testing"
)

func TestParse(t *testing.T) {
tests := []struct {
code string
value float64
}{
{"1", 1},
{"1+1", 2},
{"1+2*3", 7},
{"1+3/1", 4},
{"1-1", 0},
{"(1+2)*3", 9},
{"(1+2)/3", 1},
{"1+(2*3)", 7},
{"3*2+1", 7},
{"3*(2+1)", 9},
{"1*2*3", 6},
{"1+2+3", 6},
{"1/2", 0.5},
{"2/3", 0.6666666666666666},
}

for _, test := range tests {

if value := Parse(test.code); value != test.value {
t.Errorf("err Actual: %.16f Expect: %f", value, test.value)
} else {
t.Logf(" sucess %s = %f", test.code, test.value)
}
}
}

使用这个单元测试:

goyacc -o calcyacc.go calcyacc.y && go test -v

output

=== RUN   TestParse
calcLex_test.go:33: sucess 1 = 1.000000
calcLex_test.go:33: sucess 1+1 = 2.000000
calcLex_test.go:33: sucess 1+2*3 = 7.000000
calcLex_test.go:33: sucess 1+3/1 = 4.000000
calcLex_test.go:33: sucess 1-1 = 0.000000
calcLex_test.go:33: sucess (1+2)*3 = 9.000000
calcLex_test.go:33: sucess (1+2)/3 = 1.000000
calcLex_test.go:33: sucess 1+(2*3) = 7.000000
calcLex_test.go:33: sucess 3*2+1 = 7.000000
calcLex_test.go:33: sucess 3*(2+1) = 9.000000
calcLex_test.go:33: sucess 1*2*3 = 6.000000
calcLex_test.go:33: sucess 1+2+3 = 6.000000
calcLex_test.go:33: sucess 1/2 = 0.500000
calcLex_test.go:33: sucess 2/3 = 0.666667
--- PASS: TestParse (0.00s)
PASS
ok stgo 0.243s

References